using System;
using System.Net;
using System.Net.Sockets;
using System.Security;
using System.Threading;
using WgFrame.Utils;
using WgFrame.Net.Peer;

namespace WgFrame.Net.Socket
{
	public class SocketTcpAsync : IWgSocket, IDisposable
	{
		private class ReceiveContext
		{
			public System.Net.Sockets.Socket workSocket;

			public int ReceivedHeaderBytes;

			public byte[] HeaderBuffer;

			public int ExpectedMessageBytes;

			public int ReceivedMessageBytes;

			public byte[] MessageBuffer;

			public bool ReadingHeader 
			{ 
				get 
				{ 
					return ExpectedMessageBytes == 0; 
				} 
			}

			public bool ReadingMessage
			{ 
				get 
				{ 
					return ExpectedMessageBytes != 0; 
				}
			}

			public byte[] CurrentBuffer
            {
                get
                {
					return ReadingHeader ? HeaderBuffer : MessageBuffer;
				}
            }

			public int CurrentOffset
            {
				get
                {
					return ReadingHeader ? ReceivedHeaderBytes : ReceivedMessageBytes;
				}
            }

			public int CurrentExpected
            {
                get
                {
					return ReadingHeader ? 5 : ExpectedMessageBytes;
				}
            }

			public ReceiveContext(System.Net.Sockets.Socket socket, byte[] headerBuffer, byte[] messageBuffer)
			{
				HeaderBuffer = headerBuffer;
				MessageBuffer = messageBuffer;
				workSocket = socket;
			}

			public void Reset()
			{
				ReceivedHeaderBytes = 0;
				ExpectedMessageBytes = 0;
				ReceivedMessageBytes = 0;
			}
		}

		private System.Net.Sockets.Socket sock;

		private readonly object syncer = new object();

		public SocketTcpAsync(PeerBase npeer)
			: base(npeer)
		{
			if (ReportDebugOfLevel(DebugLevel.INFO))
			{
                Listener.DebugReturn(DebugLevel.INFO, "SocketTcpAsync, .Net, Unity.");
			}
		}

		~SocketTcpAsync()
		{
			Dispose();
		}

		public void Dispose()
		{
            State = WgSocketState.Disconnecting;
			if (sock != null)
			{
				try
				{
					if (sock.Connected)
					{
						sock.Close();
					}
				}
				catch (Exception ex)
				{
					EnqueueDebugReturn(DebugLevel.INFO, "Exception in Dispose(): " + ex);
				}
			}
			sock = null;
            State = WgSocketState.Disconnected;
		}

		public override bool Connect()
		{
			lock (syncer)
			{
				if (!base.Connect())
				{
					return false;
				}
                State = WgSocketState.Connecting;
			}
			Thread dns = new Thread(DnsAndConnect);
			dns.IsBackground = true;
			dns.Start();
			return true;
		}

		public override bool Disconnect()
		{
			if (ReportDebugOfLevel(DebugLevel.INFO))
			{
				EnqueueDebugReturn(DebugLevel.INFO, "SocketTcpAsync.Disconnect()");
			}
			lock (syncer)
			{
                State = WgSocketState.Disconnecting;
				if (sock != null)
				{
					try
					{
						sock.Close();
					}
					catch (Exception ex)
					{
						if (ReportDebugOfLevel(DebugLevel.INFO))
						{
							EnqueueDebugReturn(DebugLevel.INFO, "Exception in Disconnect(): " + ex);
						}
					}
				}
                State = WgSocketState.Disconnected;
			}
			return true;
		}

		public override WgSocketError Send(byte[] data, int length)
		{
			try
			{
				if (sock == null || !sock.Connected)
				{
					return WgSocketError.Skipped;
				}
				sock.Send(data, 0, length, SocketFlags.None);
			}
			catch (Exception e)
			{
				if (State != WgSocketState.Disconnecting && State != WgSocketState.Disconnected)
				{
					if (ReportDebugOfLevel(DebugLevel.INFO))
					{
						string socketInfo = "";
						if (sock != null)
						{
							socketInfo = string.Format(" Local: {0} Remote: {1} ({2}, {3})", sock.LocalEndPoint, sock.RemoteEndPoint, sock.Connected ? "connected" : "not connected", sock.IsBound ? "bound" : "not bound");
						}
						EnqueueDebugReturn(DebugLevel.INFO, string.Format("Cannot send to: {0} ({4}). Uptime: {1} ms. {2} {3}", ServerAddress, SupportClass.GetTickCount() - peerBase.timeBase, AddressResolvedAsIpv6 ? " IPv6" : string.Empty, socketInfo, e));
					}
					HandleException(StatusCode.SendError);
				}
				return WgSocketError.Exception;
			}
			return WgSocketError.Success;
		}

		private void DnsAndConnect()
		{
			IPAddress[] addresses = GetIpAddresses(ServerAddress);
			if (addresses == null)
			{
				return;
			}
			string messages = string.Empty;
			IPAddress[] array = addresses;
			foreach (IPAddress ipA in array)
			{
				try
				{
					sock = new System.Net.Sockets.Socket(ipA.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
					sock.NoDelay = true;
					sock.ReceiveTimeout = peerBase.DisconnectTimeout;
					sock.SendTimeout = peerBase.DisconnectTimeout;
					sock.Connect(ipA, ServerPort);
					if (sock != null && sock.Connected)
					{
						break;
					}
				}
				catch (SecurityException se2)
				{
					if (ReportDebugOfLevel(DebugLevel.ERROR))
					{
						messages = messages + se2?.ToString() + " ";
						EnqueueDebugReturn(DebugLevel.WARNING, "SecurityException catched: " + se2);
					}
				}
				catch (SocketException se)
				{
					if (ReportDebugOfLevel(DebugLevel.WARNING))
					{
						messages = messages + se?.ToString() + " " + se.ErrorCode + "; ";
						EnqueueDebugReturn(DebugLevel.WARNING, "SocketException catched: " + se?.ToString() + " ErrorCode: " + se.ErrorCode);
					}
				}
				catch (Exception e)
				{
					if (ReportDebugOfLevel(DebugLevel.WARNING))
					{
						messages = messages + e?.ToString() + "; ";
						EnqueueDebugReturn(DebugLevel.WARNING, "Exception catched: " + e);
					}
				}
			}
			if (sock == null || !sock.Connected)
			{
				if (ReportDebugOfLevel(DebugLevel.ERROR))
				{
					EnqueueDebugReturn(DebugLevel.ERROR, "Failed to connect to server after testing each known IP. Error(s): " + messages);
				}
				HandleException(StatusCode.ExceptionOnConnect);
			}
			else
			{
                AddressResolvedAsIpv6 = sock.AddressFamily == AddressFamily.InterNetworkV6;
				ServerIpAddress = sock.RemoteEndPoint.ToString();
                State = WgSocketState.Connected;
				peerBase.OnConnect();
				ReceiveAsync();
			}
		}

		private void ReceiveAsync(ReceiveContext context = null)
		{
			if (context == null)
			{
				context = new ReceiveContext(sock, new byte[5], new byte[1200]);
			}
			try
			{
				sock.BeginReceive(context.CurrentBuffer, context.CurrentOffset, context.CurrentExpected - context.CurrentOffset, SocketFlags.None, ReceiveAsync, context);
			}
			catch (Exception e)
			{
				if (State != WgSocketState.Disconnecting && State != 0)
				{
					if (ReportDebugOfLevel(DebugLevel.ERROR))
					{
						EnqueueDebugReturn(DebugLevel.ERROR, "SocketTcpAsync.ReceiveAsync Exception. State: " + State.ToString() + ". Server: '" + ServerAddress + "' Exception: " + e);
					}
					HandleException(StatusCode.ExceptionOnReceive);
				}
			}
		}

		private void ReceiveAsync(IAsyncResult ar)
		{
			if (State == WgSocketState.Disconnecting || State == WgSocketState.Disconnected)
			{
				return;
			}
			int readBytes = 0;
			try
			{
				readBytes = sock.EndReceive(ar);
				if (readBytes == 0)
				{
					throw new SocketException(10054);
				}
			}
			catch (SocketException se)
			{
				if (State != WgSocketState.Disconnecting && State != 0)
				{
					if (ReportDebugOfLevel(DebugLevel.ERROR))
					{
						EnqueueDebugReturn(DebugLevel.ERROR, "SocketTcpAsync.EndReceive SocketException. State: " + State.ToString() + ". Server: '" + ServerAddress + "' ErrorCode: " + se.ErrorCode + " SocketErrorCode: " + se.SocketErrorCode.ToString() + " Message: " + se.Message + " " + se);
					}
					HandleException(StatusCode.ExceptionOnReceive);
					return;
				}
			}
			catch (Exception e)
			{
				if (State != WgSocketState.Disconnecting && State != 0)
				{
					if (ReportDebugOfLevel(DebugLevel.ERROR))
					{
						EnqueueDebugReturn(DebugLevel.ERROR, "SocketTcpAsync.EndReceive Exception. State: " + State.ToString() + ". Server: '" + ServerAddress + "' Exception: " + e);
					}
					HandleException(StatusCode.ExceptionOnReceive);
					return;
				}
			}
			ReceiveContext context = (ReceiveContext)ar.AsyncState;
			if (readBytes + context.CurrentOffset != context.CurrentExpected)
			{
				if (context.ReadingHeader)
				{
					context.ReceivedHeaderBytes += readBytes;
				}
				else
				{
					context.ReceivedMessageBytes += readBytes;
				}
				ReceiveAsync(context);
			}
			else if (context.ReadingHeader)
			{
				byte[] headerBuff = context.HeaderBuffer;
				int length = GBitConverter.ToInt(headerBuff, 1);
				context.ExpectedMessageBytes = length;
				if (context.ExpectedMessageBytes > context.MessageBuffer.Length)
				{
					context.MessageBuffer = new byte[context.ExpectedMessageBytes];
				}
				ReceiveAsync(context);
			}
			else
			{
				HandleReceivedDatagram(context.MessageBuffer, context.ExpectedMessageBytes);
				context.Reset();
				ReceiveAsync(context);
			}
		}
	}
}
